// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.notifications;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;

import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.preferences.PrefServiceBridge;
import org.chromium.chrome.browser.preferences.Preferences;
import org.chromium.chrome.browser.preferences.PreferencesLauncher;
import org.chromium.chrome.browser.preferences.website.SingleCategoryPreferences;
import org.chromium.chrome.browser.preferences.website.SingleWebsitePreferences;
import org.chromium.chrome.browser.preferences.website.SiteSettingsCategory;
import org.chromium.chrome.browser.webapps.ChromeWebApkHost;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.webapk.lib.client.WebApkValidator;

import java.net.URI;
import java.net.URISyntaxException;

import javax.annotation.Nullable;

/**
 * Provides the ability for the NotificationPlatformBridgeAndroid to talk to the Android platform
 * notification system.
 *
 * This class should only be used on the UI thread.
 */
public class NotificationPlatformBridge {
    private static final String TAG = NotificationPlatformBridge.class.getSimpleName();

    // We always use the same integer id when showing and closing notifications. The notification
    // tag is always set, which is a safe and sufficient way of identifying a notification, so the
    // integer id is not needed anymore except it must not vary in an uncontrolled way.
    @VisibleForTesting static final int PLATFORM_ID = -1;

    // Prefix for platform tags generated by this class. This allows us to verify when reading a tag
    // that it was set by us.
    private static final String PLATFORM_TAG_PREFIX =
            NotificationPlatformBridge.class.getSimpleName();

    // We always use the same request code for pending intents. We use other ways to force
    // uniqueness of pending intents when necessary.
    private static final int PENDING_INTENT_REQUEST_CODE = 0;

    private static final int[] EMPTY_VIBRATION_PATTERN = new int[0];

    private static NotificationPlatformBridge sInstance;
    private static NotificationManagerProxy sNotificationManagerOverride;

    private final long mNativeNotificationPlatformBridge;

    private final Context mAppContext;
    private final NotificationManagerProxy mNotificationManager;

    private long mLastNotificationClickMs = 0L;

    /**
     * Creates a new instance of the NotificationPlatformBridge.
     *
     * @param nativeNotificationPlatformBridge Instance of the NotificationPlatformBridgeAndroid
     *        class.
     */
    @CalledByNative
    private static NotificationPlatformBridge create(long nativeNotificationPlatformBridge) {
        if (sInstance != null) {
            throw new IllegalStateException(
                "There must only be a single NotificationPlatformBridge.");
        }

        sInstance = new NotificationPlatformBridge(nativeNotificationPlatformBridge);
        return sInstance;
    }

    /**
     * Returns the current instance of the NotificationPlatformBridge.
     *
     * @return The instance of the NotificationPlatformBridge, if any.
     */
    @Nullable
    @VisibleForTesting
    static NotificationPlatformBridge getInstanceForTests() {
        return sInstance;
    }

    /**
     * Overrides the notification manager which is to be used for displaying Notifications on the
     * Android framework. Should only be used for testing. Tests are expected to clean up after
     * themselves by setting this to NULL again.
     *
     * @param proxy The notification manager instance to use instead of the system's.
     */
    @VisibleForTesting
    public static void overrideNotificationManagerForTesting(
            NotificationManagerProxy notificationManager) {
        sNotificationManagerOverride = notificationManager;
    }

    private NotificationPlatformBridge(long nativeNotificationPlatformBridge) {
        mNativeNotificationPlatformBridge = nativeNotificationPlatformBridge;
        mAppContext = ContextUtils.getApplicationContext();

        if (sNotificationManagerOverride != null) {
            mNotificationManager = sNotificationManagerOverride;
        } else {
            mNotificationManager = new NotificationManagerProxyImpl(
                    (NotificationManager) mAppContext.getSystemService(
                            Context.NOTIFICATION_SERVICE));
        }
    }

    /**
     * Marks the current instance as being freed, allowing for a new NotificationPlatformBridge
     * object to be initialized.
     */
    @CalledByNative
    private void destroy() {
        assert sInstance == this;
        sInstance = null;
    }

    /**
     * Returns the package for the WebAPK which should handle the URL.
     *
     * @param url The url to check.
     * @return Package name of the WebAPK which should handle the URL. Returns empty string if the
     *         URL should not be handled by a WebAPK.
     */
    @CalledByNative
    private String queryWebApkPackage(String url) {
        if (!ChromeWebApkHost.isEnabled()) return "";

        String webApkPackage =
                WebApkValidator.queryWebApkPackage(mAppContext, url);
        return webApkPackage == null ? "" : webApkPackage;
    }

    /**
     * Invoked by the NotificationService when a Notification intent has been received. There may
     * not be an active instance of the NotificationPlatformBridge at this time, so inform the
     * native side through a static method, initializing both ends if needed.
     *
     * @param intent The intent as received by the Notification service.
     * @return Whether the event could be handled by the native Notification bridge.
     */
    public static boolean dispatchNotificationEvent(Intent intent) {
        if (sInstance == null) {
            nativeInitializeNotificationPlatformBridge();
            if (sInstance == null) {
                Log.e(TAG, "Unable to initialize the native NotificationPlatformBridge.");
                return false;
            }
        }

        String notificationId = intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_ID);

        String origin = intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN);
        String profileId =
                intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_ID);
        boolean incognito = intent.getBooleanExtra(
                NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_INCOGNITO, false);
        String tag = intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_TAG);

        Log.i(TAG, "Dispatching notification event to native: " + notificationId);

        if (NotificationConstants.ACTION_CLICK_NOTIFICATION.equals(intent.getAction())) {
            String webApkPackage = "";
            if (ChromeWebApkHost.isEnabled()) {
                webApkPackage = intent.getStringExtra(
                    NotificationConstants.EXTRA_NOTIFICATION_INFO_WEBAPK_PACKAGE);
                if (webApkPackage == null) {
                    webApkPackage = "";
                }
            }
            int actionIndex = intent.getIntExtra(
                    NotificationConstants.EXTRA_NOTIFICATION_INFO_ACTION_INDEX, -1);
            sInstance.onNotificationClicked(notificationId, origin, profileId, incognito, tag,
                    webApkPackage, actionIndex, getNotificationReply(intent));
            return true;
        } else if (NotificationConstants.ACTION_CLOSE_NOTIFICATION.equals(intent.getAction())) {
            // Notification deleteIntent is executed only "when the notification is explicitly
            // dismissed by the user, either with the 'Clear All' button or by swiping it away
            // individually" (though a third-party NotificationListenerService may also trigger it).
            sInstance.onNotificationClosed(
                    notificationId, origin, profileId, incognito, tag, true /* byUser */);
            return true;
        }

        Log.e(TAG, "Unrecognized Notification action: " + intent.getAction());
        return false;
    }

    @Nullable
    private static String getNotificationReply(Intent intent) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
            // RemoteInput was added in KITKAT_WATCH.
            Bundle remoteInputResults = RemoteInput.getResultsFromIntent(intent);
            if (remoteInputResults != null) {
                CharSequence reply =
                        remoteInputResults.getCharSequence(NotificationConstants.KEY_TEXT_REPLY);
                if (reply != null) {
                    return reply.toString();
                }
            }
        }
        return null;
    }

    /**
     * Launches the notifications preferences screen. If the received intent indicates it came
     * from the gear button on a flipped notification, this launches the site specific preferences
     * screen.
     *
     * @param context The context that received the intent.
     * @param incomingIntent The received intent.
     */
    public static void launchNotificationPreferences(Context context, Intent incomingIntent) {
        // This method handles an intent fired by the Android system. There is no guarantee that the
        // native library is loaded at this point. The native library is needed for the preferences
        // activity, and it loads the library, but there are some native calls even before that
        // activity is started: from RecordUserAction.record and (indirectly) from
        // UrlFormatter.formatUrlForSecurityDisplay.
        try {
            ChromeBrowserInitializer.getInstance(context).handleSynchronousStartup();
        } catch (ProcessInitException e) {
            Log.e(TAG, "Failed to start browser process.", e);
            // The library failed to initialize and nothing in the application can work, so kill
            // the whole application.
            System.exit(-1);
            return;
        }

        // Use the application context because it lives longer. When using the given context, it
        // may be stopped before the preferences intent is handled.
        Context applicationContext = context.getApplicationContext();

        // If we can read an origin from the intent, use it to open the settings screen for that
        // origin.
        String origin = getOriginFromTag(
                incomingIntent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_TAG));
        boolean launchSingleWebsitePreferences = origin != null;

        String fragmentName = launchSingleWebsitePreferences
                ? SingleWebsitePreferences.class.getName()
                : SingleCategoryPreferences.class.getName();
        Intent preferencesIntent =
                PreferencesLauncher.createIntentForSettingsPage(applicationContext, fragmentName);

        Bundle fragmentArguments;
        if (launchSingleWebsitePreferences) {
            // Record that the user has clicked on the [Site Settings] button.
            RecordUserAction.record("Notifications.ShowSiteSettings");

            // All preferences for a specific origin.
            fragmentArguments = SingleWebsitePreferences.createFragmentArgsForSite(origin);
        } else {
            // Notification preferences for all origins.
            fragmentArguments = new Bundle();
            fragmentArguments.putString(SingleCategoryPreferences.EXTRA_CATEGORY,
                    SiteSettingsCategory.CATEGORY_NOTIFICATIONS);
            fragmentArguments.putString(SingleCategoryPreferences.EXTRA_TITLE,
                    applicationContext.getResources().getString(
                            R.string.push_notifications_permission_title));
        }
        preferencesIntent.putExtra(Preferences.EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArguments);

        // We need to ensure that no existing preference tasks are being re-used in order for the
        // new activity to appear on top.
        preferencesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);

        applicationContext.startActivity(preferencesIntent);
    }

    /**
     * Returns a bogus Uri used to make each intent unique according to Intent#filterEquals.
     * Without this, the pending intents derived from the intent may be reused, because extras are
     * not taken into account for the filterEquals comparison.
     *
     * @param notificationId The id of the notification.
     * @param origin The origin to whom the notification belongs.
     * @param actionIndex The zero-based index of the action button, or -1 if not applicable.
     */
    private Uri makeIntentData(String notificationId, String origin, int actionIndex) {
        return Uri.parse(origin).buildUpon().fragment(notificationId + "," + actionIndex).build();
    }

    /**
     * Returns the PendingIntent for completing |action| on the notification identified by the data
     * in the other parameters.
     *
     * @param action The action this pending intent will represent.
     * @paramn notificationId The id of the notification.
     * @param origin The origin to whom the notification belongs.
     * @param profileId Id of the profile to which the notification belongs.
     * @param incognito Whether the profile was in incognito mode.
     * @param tag The tag of the notification. May be NULL.
     * @param webApkPackage The package of the WebAPK associated with the notification. Empty if
     *        the notification is not associated with a WebAPK.
     * @param actionIndex The zero-based index of the action button, or -1 if not applicable.
     */
    private PendingIntent makePendingIntent(String action, String notificationId, String origin,
            String profileId, boolean incognito, @Nullable String tag, String webApkPackage,
            int actionIndex) {
        Uri intentData = makeIntentData(notificationId, origin, actionIndex);
        Intent intent = new Intent(action, intentData);
        intent.setClass(mAppContext, NotificationService.Receiver.class);

        intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_ID, notificationId);
        intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN, origin);
        intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_ID, profileId);
        intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_INCOGNITO, incognito);
        intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_TAG, tag);
        intent.putExtra(
                NotificationConstants.EXTRA_NOTIFICATION_INFO_WEBAPK_PACKAGE, webApkPackage);
        intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ACTION_INDEX, actionIndex);

        return PendingIntent.getBroadcast(mAppContext, PENDING_INTENT_REQUEST_CODE, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
    }

    /**
     * Generates the tag to be passed to the notification manager.
     *
     * If the generated tag is the same as that of a previous notification, a new notification shown
     * with this tag will replace it.
     *
     * If the input tag is not empty the output is: PREFIX + SEPARATOR + ORIGIN + SEPARATOR + TAG.
     * This output will be the same for notifications from the same origin that have the same input
     * tag.
     *
     * If the input tag is empty the output is PREFIX + SEPARATOR + ORIGIN + SEPARATOR +
     * NOTIFICATION_ID.
     *
     * @param notificationId The id of the notification.
     * @param origin The origin for which the notification is shown.
     * @param tag A string identifier for this notification.
     * @return The generated platform tag.
     */
    @VisibleForTesting
    static String makePlatformTag(String notificationId, String origin, @Nullable String tag) {
        // The given tag may contain the separator character, so add it last to make reading the
        // preceding origin token reliable. If no tag was specified (it is the default empty
        // string), make the platform tag unique by appending the notification id.
        StringBuilder builder = new StringBuilder();
        builder.append(PLATFORM_TAG_PREFIX)
                .append(NotificationConstants.NOTIFICATION_TAG_SEPARATOR)
                .append(origin)
                .append(NotificationConstants.NOTIFICATION_TAG_SEPARATOR);

        if (TextUtils.isEmpty(tag)) {
            builder.append(notificationId);
        } else {
            builder.append(tag);
        }

        return builder.toString();
    }

    /**
     * Attempts to extract an origin from the tag extra in the given intent.
     *
     * See {@link #makePlatformTag} for details about the format of the tag.
     *
     * @param tag The tag from the intent extra. May be null.
     * @return The origin string. Returns null if there was no tag extra in the given intent, or if
     *         the tag value did not match the expected format.
     */
    @Nullable
    @VisibleForTesting
    static String getOriginFromTag(@Nullable String tag) {
        // If the user touched the settings cog on a flipped notification originating from this
        // class, there will be a notification tag extra in a specific format. From the tag we can
        // read the origin of the notification.
        if (tag == null || !tag.startsWith(PLATFORM_TAG_PREFIX)) return null;

        String[] parts = tag.split(NotificationConstants.NOTIFICATION_TAG_SEPARATOR);
        assert parts.length >= 3;
        try {
            URI uri = new URI(parts[1]);
            if (uri.getHost() != null) return parts[1];
        } catch (URISyntaxException e) {
            Log.e(TAG, "Expected to find a valid url in the notification tag extra.", e);
            return null;
        }
        return null;
    }

    /**
     * Generates the notification defaults from vibrationPattern's size and silent.
     *
     * Use the system's default ringtone, vibration and indicator lights unless the notification
     * has been marked as being silent.
     * If a vibration pattern is set, the notification should use the provided pattern
     * rather than defaulting to the system settings.
     *
     * @param vibrationPatternLength Vibration pattern's size for the Notification.
     * @param silent Whether the default sound, vibration and lights should be suppressed.
     * @param vibrateEnabled Whether vibration is enabled in preferences.
     * @return The generated notification's default value.
    */
    @VisibleForTesting
    static int makeDefaults(int vibrationPatternLength, boolean silent, boolean vibrateEnabled) {
        assert !silent || vibrationPatternLength == 0;

        if (silent) return 0;

        int defaults = Notification.DEFAULT_ALL;
        if (vibrationPatternLength > 0 || !vibrateEnabled) {
            defaults &= ~Notification.DEFAULT_VIBRATE;
        }
        return defaults;
    }

    /**
     * Generates the vibration pattern used in Android notification.
     *
     * Android takes a long array where the first entry indicates the number of milliseconds to wait
     * prior to starting the vibration, whereas Chrome follows the syntax of the Web Vibration API.
     *
     * @param vibrationPattern Vibration pattern following the Web Vibration API syntax.
     * @return Vibration pattern following the Android syntax.
    */
    @VisibleForTesting
    static long[] makeVibrationPattern(int[] vibrationPattern) {
        long[] pattern = new long[vibrationPattern.length + 1];
        for (int i = 0; i < vibrationPattern.length; ++i) {
            pattern[i + 1] = vibrationPattern[i];
        }
        return pattern;
    }

    /**
     * Displays a notification with the given details.
     *
     * @param notificationId The id of the notification.
     * @param origin Full text of the origin, including the protocol, owning this notification.
     * @param profileId Id of the profile that showed the notification.
     * @param incognito if the session of the profile is an off the record one.
     * @param tag A string identifier for this notification. If the tag is not empty, the new
     *            notification will replace the previous notification with the same tag and origin,
     *            if present. If no matching previous notification is present, the new one will just
     *            be added.
     * @param webApkPackage The package of the WebAPK associated with the notification. Empty if
     *        the notification is not associated with a WebAPK.
     * @param title Title to be displayed in the notification.
     * @param body Message to be displayed in the notification. Will be trimmed to one line of
     *             text by the Android notification system.
     * @param image Content image to be prominently displayed when the notification is expanded.
     * @param icon Icon to be displayed in the notification. Valid Bitmap icons will be scaled to
     *             the platforms, whereas a default icon will be generated for invalid Bitmaps.
     * @param badge An image to represent the notification in the status bar. It is also displayed
     *              inside the notification.
     * @param vibrationPattern Vibration pattern following the Web Vibration syntax.
     * @param timestamp The timestamp of the event for which the notification is being shown.
     * @param renotify Whether the sound, vibration, and lights should be replayed if the
     *                 notification is replacing another notification.
     * @param silent Whether the default sound, vibration and lights should be suppressed.
     * @param actions Action buttons to display alongside the notification.
     * @see https://developer.android.com/reference/android/app/Notification.html
     */
    @CalledByNative
    private void displayNotification(String notificationId, String origin, String profileId,
            boolean incognito, String tag, String webApkPackage, String title, String body,
            Bitmap image, Bitmap icon, Bitmap badge, int[] vibrationPattern, long timestamp,
            boolean renotify, boolean silent, ActionInfo[] actions) {
        Resources res = mAppContext.getResources();

        // Record whether it's known whether notifications can be shown to the user at all.
        RecordHistogram.recordEnumeratedHistogram(
                "Notifications.AppNotificationStatus",
                NotificationSystemStatusUtil.determineAppNotificationStatus(mAppContext),
                NotificationSystemStatusUtil.APP_NOTIFICATIONS_STATUS_BOUNDARY);

        // Set up a pending intent for going to the settings screen for |origin|.
        Intent settingsIntent = PreferencesLauncher.createIntentForSettingsPage(
                mAppContext, SingleWebsitePreferences.class.getName());
        settingsIntent.setData(makeIntentData(notificationId, origin, -1 /* actionIndex */));
        settingsIntent.putExtra(Preferences.EXTRA_SHOW_FRAGMENT_ARGUMENTS,
                SingleWebsitePreferences.createFragmentArgsForSite(origin));

        PendingIntent pendingSettingsIntent = PendingIntent.getActivity(mAppContext,
                PENDING_INTENT_REQUEST_CODE, settingsIntent, PendingIntent.FLAG_UPDATE_CURRENT);

        PendingIntent clickIntent =
                makePendingIntent(NotificationConstants.ACTION_CLICK_NOTIFICATION, notificationId,
                        origin, profileId, incognito, tag, webApkPackage, -1 /* actionIndex */);
        PendingIntent closeIntent =
                makePendingIntent(NotificationConstants.ACTION_CLOSE_NOTIFICATION, notificationId,
                        origin, profileId, incognito, tag, webApkPackage, -1 /* actionIndex */);

        boolean hasImage = image != null;

        NotificationBuilderBase notificationBuilder =
                createNotificationBuilder(hasImage)
                        .setTitle(title)
                        .setBody(body)
                        .setImage(image)
                        .setLargeIcon(icon)
                        .setSmallIcon(R.drawable.ic_chrome)
                        .setSmallIcon(badge)
                        .setContentIntent(clickIntent)
                        .setDeleteIntent(closeIntent)
                        .setTicker(createTickerText(title, body))
                        .setTimestamp(timestamp)
                        .setRenotify(renotify)
                        .setOrigin(UrlFormatter.formatUrlForSecurityDisplay(
                                origin, false /* showScheme */));

        for (int actionIndex = 0; actionIndex < actions.length; actionIndex++) {
            PendingIntent intent = makePendingIntent(
                    NotificationConstants.ACTION_CLICK_NOTIFICATION, notificationId, origin,
                    profileId, incognito, tag, webApkPackage, actionIndex);
            ActionInfo action = actions[actionIndex];
            // Don't show action button icons when there's an image, as then action buttons go on
            // the same row as the Site Settings button, so icons wouldn't leave room for text.
            Bitmap actionIcon = hasImage ? null : action.icon;
            if (action.type == NotificationActionType.TEXT) {
                notificationBuilder.addTextAction(
                        actionIcon, action.title, intent, action.placeholder);
            } else {
                notificationBuilder.addButtonAction(actionIcon, action.title, intent);
            }
        }

        // If action buttons are displayed, there isn't room for the full Site Settings button
        // label and icon, so abbreviate it. This has the unfortunate side-effect of unnecessarily
        // abbreviating it on Android Wear also (crbug.com/576656). If custom layouts are enabled,
        // the label and icon provided here only affect Android Wear, so don't abbreviate them.
        boolean abbreviateSiteSettings = actions.length > 0 && !useCustomLayouts(hasImage);
        int settingsIconId = abbreviateSiteSettings ? 0 : R.drawable.settings_cog;
        CharSequence settingsTitle = abbreviateSiteSettings
                                     ? res.getString(R.string.notification_site_settings_button)
                                     : res.getString(R.string.page_info_site_settings_button);
        // If the settings button is displayed together with the other buttons it has to be the last
        // one, so add it after the other actions.
        notificationBuilder.addSettingsAction(settingsIconId, settingsTitle, pendingSettingsIntent);

        // The Android framework applies a fallback vibration pattern for the sound when the device
        // is in vibrate mode, there is no custom pattern, and the vibration default has been
        // disabled. To truly prevent vibration, provide a custom empty pattern.
        boolean vibrateEnabled = PrefServiceBridge.getInstance().isNotificationsVibrateEnabled();
        if (!vibrateEnabled) {
            vibrationPattern = EMPTY_VIBRATION_PATTERN;
        }
        notificationBuilder.setDefaults(
                makeDefaults(vibrationPattern.length, silent, vibrateEnabled));
        notificationBuilder.setVibrate(makeVibrationPattern(vibrationPattern));

        String platformTag = makePlatformTag(notificationId, origin, tag);
        if (webApkPackage.isEmpty()) {
            mNotificationManager.notify(platformTag, PLATFORM_ID, notificationBuilder.build());
        } else {
            WebApkNotificationClient.notifyNotification(
                    webApkPackage, notificationBuilder, platformTag, PLATFORM_ID);
        }
    }

    private NotificationBuilderBase createNotificationBuilder(boolean hasImage) {
        if (useCustomLayouts(hasImage)) {
            return new CustomNotificationBuilder(mAppContext);
        }
        return new StandardNotificationBuilder(mAppContext);
    }

    /**
     * Creates the ticker text for a notification having |title| and |body|. The notification's
     * title will be printed in bold, followed by the text of the body.
     *
     * @param title Title of the notification.
     * @param body Textual contents of the notification.
     * @return A character sequence containing the ticker's text.
     */
    private CharSequence createTickerText(String title, String body) {
        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();

        spannableStringBuilder.append(title);
        spannableStringBuilder.append("\n");
        spannableStringBuilder.append(body);

        // Mark the title of the notification as being bold.
        spannableStringBuilder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD),
                0, title.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);

        return spannableStringBuilder;
    }

    /**
     * Determines whether to use standard notification layouts, using NotificationCompat.Builder,
     * or custom layouts using Chrome's own templates.
     *
     * The --{enable,disable}-web-notification-custom-layouts command line flags take precedence.
     *
     * Normally a standard layout is used on Android N+, and a custom layout is used on older
     * versions of Android. But if the notification has a content image, there isn't enough room for
     * the Site Settings button to go on its own line when showing an image, nor is there enough
     * room for action button icons, so a standard layout will be used here even on old versions.
     *
     * @param hasImage Whether the notification has a content image.
     * @return Whether custom layouts should be used.
     */
    @VisibleForTesting
    static boolean useCustomLayouts(boolean hasImage) {
        CommandLine commandLine = CommandLine.getInstance();
        if (commandLine.hasSwitch(ChromeSwitches.ENABLE_WEB_NOTIFICATION_CUSTOM_LAYOUTS)) {
            return true;
        }
        if (commandLine.hasSwitch(ChromeSwitches.DISABLE_WEB_NOTIFICATION_CUSTOM_LAYOUTS)) {
            return false;
        }
        if (Build.VERSION.CODENAME.equals("N") || Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
            return false;
        }
        if (hasImage) {
            return false;
        }
        return true;
    }

    /**
     * Returns whether a notification has been clicked in the last 5 seconds.
     * Used for Startup.BringToForegroundReason UMA histogram.
     */
    public static boolean wasNotificationRecentlyClicked() {
        if (sInstance == null) return false;
        long now = System.currentTimeMillis();
        return now - sInstance.mLastNotificationClickMs < 5 * 1000;
    }

    /**
     * Closes the notification associated with the given parameters.
     *
     * @param profileId of the profile whose notification this is for.
     * @param notificationId The id of the notification.
     * @param origin The origin to which the notification belongs.
     * @param tag The tag of the notification. May be NULL.
     * @param webApkPackage The package of the WebAPK associated with the notification.
     *        Empty if the notification is not associated with a WebAPK.
     */
    @CalledByNative
    private void closeNotification(String profileId, String notificationId, String origin,
            String tag, String webApkPackage) {
        // TODO(miguelg) make profile_id part of the tag.
        String platformTag = makePlatformTag(notificationId, origin, tag);

        if (webApkPackage.isEmpty()) {
            mNotificationManager.cancel(platformTag, PLATFORM_ID);
        } else {
            WebApkNotificationClient.cancelNotification(webApkPackage, platformTag, PLATFORM_ID);
        }
    }

    /**
     * Calls NotificationPlatformBridgeAndroid::OnNotificationClicked in native code to indicate
     * that the notification with the given parameters has been clicked on.
     *
     * @param notificationId The id of the notification.
     * @param origin The origin of the notification.
     * @param profileId Id of the profile that showed the notification.
     * @param incognito if the profile session was an off the record one.
     * @param tag The tag of the notification. May be NULL.
     * @param webApkPackage The package of the WebAPK associated with the notification.
     *        Empty if the notification is not associated with a WebAPK.
     * @param actionIndex
     * @param reply User reply to a text action on the notification. Null if the user did not click
     *              on a text action or if inline replies are not supported.
     */
    private void onNotificationClicked(String notificationId, String origin, String profileId,
            boolean incognito, String tag, String webApkPackage, int actionIndex,
            @Nullable String reply) {
        mLastNotificationClickMs = System.currentTimeMillis();
        nativeOnNotificationClicked(mNativeNotificationPlatformBridge, notificationId, origin,
                profileId, incognito, tag, webApkPackage, actionIndex, reply);
    }

    /**
     * Calls NotificationPlatformBridgeAndroid::OnNotificationClosed in native code to indicate that
     * the notification with the given parameters has been closed.
     *
     * @param notificationId The id of the notification.
     * @param origin The origin of the notification.
     * @param profileId Id of the profile that showed the notification.
     * @param incognito if the profile session was an off the record one.
     * @param tag The tag of the notification. May be NULL.
     * @param byUser Whether the notification was closed by a user gesture.
     */
    private void onNotificationClosed(String notificationId, String origin, String profileId,
            boolean incognito, String tag, boolean byUser) {
        nativeOnNotificationClosed(mNativeNotificationPlatformBridge, notificationId, origin,
                profileId, incognito, tag, byUser);
    }

    private static native void nativeInitializeNotificationPlatformBridge();

    private native void nativeOnNotificationClicked(long nativeNotificationPlatformBridgeAndroid,
            String notificationId, String origin, String profileId, boolean incognito, String tag,
            String webApkPackage, int actionIndex, String reply);
    private native void nativeOnNotificationClosed(long nativeNotificationPlatformBridgeAndroid,
            String notificationId, String origin, String profileId, boolean incognito, String tag,
            boolean byUser);
}
